package com.raise.drive.model.dao.impl;

import com.raise.drive.model.search.SearchRequest;
import com.raise.drive.model.dao.BaseDao;
import org.apache.log4j.Logger;
import org.hibernate.FetchMode;
import org.hibernate.criterion.*;
import org.springframework.beans.BeanUtils;
import org.springframework.orm.ObjectRetrievalFailureException;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;


/**
 * Author: Nazica
 */
public abstract class BaseDaoImpl<T> extends HibernateDaoSupport implements BaseDao<T> {

    private static final Logger log = Logger.getLogger(BaseDaoImpl.class);

    protected SearchRequest getDefaultRequest() {
        SearchRequest request = new SearchRequest();

        List<String> fieldNames = new ArrayList<String>();
        Class superClazz = getDomainClass();
        while(superClazz != Object.class) {
            for(Field field : superClazz.getDeclaredFields()) {
                fieldNames.add(field.getName());
            }
            superClazz = superClazz.getSuperclass();
        }

        request.setEagerPaths(fieldNames.toArray(new String[fieldNames.size()]));
        return request;
    }

    /**
     * Returns order of sorting of request results.
     *
     * @param request - request to get order from
     * @return order of the sorting of request results
     */
    protected Order getOrder(SearchRequest request) {
        String sortedField = request.getSortedField();
        return request.isDesc() ? Order.desc(sortedField) : Order.asc(sortedField);
    }

    /**
     * Base method for extracting object that bases on object class and id.
     * If no object found returns null
     *
     * @param id the identifier (primary key) of the class
     * @return filled object
     */
    protected T getObject(Serializable id) {
        return (T) getHibernateTemplate().get(getDomainClass(), id);
    }

    /**
     * Base method for extracting object that bases on it class and id.
     * If no object found throws ObjectRetrievalFailureException
     *
     * @param id the identifier (primary key) of the class
     * @return filled object
     * @throws ObjectRetrievalFailureException
     *          if object not found
     * @see ObjectRetrievalFailureException
     */
    protected T getRequiredObject(Serializable id) throws ObjectRetrievalFailureException {
        T object = (T) getHibernateTemplate().get(getDomainClass(), id);
        if (null == object) {
            throw new ObjectRetrievalFailureException(getDomainClass(), id);
        }
        return object;
    }

    public T getObjectByCondition(String fieldName, Object fieldValue) {
        DetachedCriteria criteria = DetachedCriteria.forClass(getDomainClass()).add(
                Restrictions.eq(fieldName, fieldValue));
        List<T> list = getHibernateTemplate().findByCriteria(criteria, 0, 1);
        return list.isEmpty() ? null : list.get(0);
    }

    /**
     * Get objects by specified search request, that supports pagination
     *
     * @return list of objects
     */
    public List<T> getObjects() {
        return getHibernateTemplate().loadAll(getDomainClass());
    }

    /**
     * Get count of all objects of given type.
     *
     * @return count of objects
     */
    public int getAllObjectsCount() {
        DetachedCriteria criteria =
                DetachedCriteria.forClass(getDomainClass()).setProjection(Projections.rowCount());
        return (Integer) getHibernateTemplate().findByCriteria(criteria).get(0);
    }

    /**
     * Get count of object by given criteria
     *
     * @param criteria
     * @return int count of found object
     */
    protected int getObjectsCount(DetachedCriteria criteria) {
        criteria.setProjection(Projections.rowCount());
        return (Integer) getHibernateTemplate().findByCriteria(criteria).get(0);
    }


    /**
     * Base method for object saving (controls object inserting and updating).
     *
     * @param object object to save
     */
    protected void saveObject(T object) {
        getHibernateTemplate().saveOrUpdate(object);
    }

    /**
     * Base method for object deleting that bases on object class and id.
     *
     * @param id - an id (primary key) of the object
     */
    protected void deleteObject(Serializable id) {
        getHibernateTemplate().delete(getObject(id));
    }

    /**
     * Base method for deleting a specified object
     *
     * @param object a object to delete
     */
    protected void deleteObject(T object) {
        getHibernateTemplate().delete(object);
    }

    protected abstract Class<T> getDomainClass();

    /**
     * Find objects by given request
     *
     * @param searchRequest
     * @param criteria
     * @return List of found objects
     */
    public List<T> getObjectsByRequest(SearchRequest searchRequest, DetachedCriteria criteria) {
        int start = searchRequest.getStart();
        int limit = searchRequest.getLimit();
        String sortedField = searchRequest.getSortedField();
        boolean desc = searchRequest.isDesc();

        if (null != BeanUtils.getPropertyDescriptor(getDomainClass(), sortedField)) {
            Order order = desc ? Order.desc(sortedField) : Order.asc(sortedField);
            criteria.addOrder(order);
        }
        return getHibernateTemplate().findByCriteria(criteria, start, limit);
    }

    /**
     * Specified the fields that should be not null in query.
     *
     * @param junction   junction to put not null-fields
     * @param notNullFields fields that should be not null
     * @return specified junction
     */
    protected Junction addNotNullFieldCriterias(Junction junction, List<String> notNullFields) {
        for (String field : notNullFields) {
            junction.add(Restrictions.isNotNull(field));
        }
        return junction;
    }

    /**
     * Specified the fields that should be null in query.
     *
     * @param junction   junction to put null-fields
     * @param nullFields fields that should be null
     * @return specified junction
     */
    protected Junction addNullFieldCriterias(Junction junction, List<String> nullFields) {
        for (String field : nullFields) {
            junction.add(Restrictions.isNull(field));
        }
        return junction;
    }

    /**
     * Adds "not equal" criterias from specified map to specified junction.
     *
     * @param junction    junction to add equal criterias
     * @param notEqualMap map to add "not equal" criterias from
     * @return specified junction with added "not equal" criterias
     */
    protected Junction addNotEqualsCriterias(Junction junction, Map<String, Object> notEqualMap) {
        for (String key : notEqualMap.keySet()) {
            Object value = notEqualMap.get(key);
            if (value != null) {
                junction.add(Restrictions.ne(key, value));
            }
        }
        return junction;
    }

    /**
     * Adds criterias from specified map to specified junction.
     *
     * @param junction junction to add equal criterias
     * @param equalMap map to add equal criterias from
     * @return specified junction with added equal criterias
     */
    protected Junction addEqualsCriterias(Junction junction, Map<String, Object> equalMap) {
        return junction.add(Restrictions.allEq(equalMap));
    }

    /**
     * Puts eager paths(fields that are fetched nevertheless they are lazy).
     *
     * @param criteria   criteria to put eager paths to
     * @param eagerPaths eager paths(fields that are fetched nevertheless they are lazy)
     * @return specified criteria with added eager paths
     */
    protected DetachedCriteria setEagerPaths(DetachedCriteria criteria, String[] eagerPaths) {
        if (eagerPaths != null) {
            for (String path : eagerPaths) {
                criteria.setFetchMode(path, FetchMode.JOIN);
            }
        }
        return criteria;
    }

    /**
     * Adds criterias from specified map to specified junction.
     *
     * @param junction junction to add criteria to
     * @param likeMap  map to add like criterias from
     * @return specified junction
     */
    protected Junction addLikeCriterias(Junction junction, Map<String, String> likeMap) {
        for (String key : likeMap.keySet()) {
            String value = likeMap.get(key);
            if (value != null) {
                junction.add(Restrictions.like(key, value, MatchMode.ANYWHERE));
            }
        }
        return junction;
    }

    /**
     * Defines the junction type(disjunction or conjunction) by its integer representation.
     *
     * @param junctionType integer representation of the junction type.
     *                     <br><tt>SearchRequest.SEARCH_AND</tt> - represents AND operation
     *                     <br><tt>SearchRequest.SEARCH_OR</tt> - represents OR operation
     * @return - junction type(operations OR, AND)
     */
    protected Junction getJunction(int junctionType) {
        Junction junction;
        if (junctionType == SearchRequest.SEARCH_OR) {
            junction = Restrictions.disjunction();
        } else {
            junction = Restrictions.conjunction();
        }
        return junction;
    }

    /**
     * Defines the order of fetched data.
     *
     * @param desc        - <tt>true</tt> if the data should have descending order
     * @param sortedField - the field to sort data by
     * @return order of the fetched data
     */
    protected Order getOrder(boolean desc, String sortedField) {
        Order order;
        if (desc) {
            order = Order.desc(sortedField);
        } else {
            order = Order.asc(sortedField);
        }
        return order;
    }
}